/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.solr.util;

import java.io.Reader;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.MalformedURLException;
import java.util.Collection;
import java.util.Map;
import java.util.HashSet;
import java.util.HashMap;
import java.util.Enumeration;
import java.util.jar.*;

/**
 * Given a list of Jar files, suggest missing analysis factories.
 * 
 * @version $Id: SuggestMissingFactories.java 701485 2008-10-03 18:43:57Z ryan $
 */
public class SuggestMissingFactories {

	public static void main(String[] args) throws ClassNotFoundException,
			IOException, NoSuchMethodException {

		final File[] files = new File[args.length];
		for (int i = 0; i < args.length; i++) {
			files[i] = new File(args[i]);
		}
		final FindClasses finder = new FindClasses(files);
		final ClassLoader cl = finder.getClassLoader();

		final Class TOKENSTREAM = cl
				.loadClass("org.apache.lucene.analysis.TokenStream");
		final Class TOKENIZER = cl
				.loadClass("org.apache.lucene.analysis.Tokenizer");
		final Class TOKENFILTER = cl
				.loadClass("org.apache.lucene.analysis.TokenFilter");
		final Class TOKENIZERFACTORY = cl
				.loadClass("org.apache.solr.analysis.TokenizerFactory");
		final Class TOKENFILTERFACTORY = cl
				.loadClass("org.apache.solr.analysis.TokenFilterFactory");

		final HashSet<Class> result = new HashSet<Class>(
				finder.findExtends(TOKENIZER));
		result.addAll(finder.findExtends(TOKENFILTER));

		result.removeAll(finder.findMethodReturns(
				finder.findExtends(TOKENIZERFACTORY), "create", Reader.class)
				.values());
		result.removeAll(finder.findMethodReturns(
				finder.findExtends(TOKENFILTERFACTORY), "create", TOKENSTREAM)
				.values());

		for (final Class c : result) {
			System.out.println(c.getName());
		}
	}

}

/**
 * Takes in a clazz name and a jar and finds all classes in that jar that extend
 * clazz.
 */
class FindClasses {

	/**
	 * Simple command line test method
	 */
	public static void main(String[] args) throws ClassNotFoundException,
			IOException, NoSuchMethodException {

		FindClasses finder = new FindClasses(new File(args[1]));
		ClassLoader cl = finder.getClassLoader();
		Class clazz = cl.loadClass(args[0]);
		if (args.length == 2) {

			System.out.println("Finding all extenders of " + clazz.getName());
			for (Class c : finder.findExtends(clazz)) {
				System.out.println(c.getName());
			}
		} else {
			String methName = args[2];
			System.out.println("Finding all extenders of " + clazz.getName()
					+ " with method: " + methName);

			Class[] methArgs = new Class[args.length - 3];
			for (int i = 3; i < args.length; i++) {
				methArgs[i - 3] = cl.loadClass(args[i]);
			}
			Map<Class, Class> map = finder.findMethodReturns(
					finder.findExtends(clazz), methName, methArgs);

			for (Class key : map.keySet()) {
				System.out.println(key.getName() + " => "
						+ map.get(key).getName());
			}

		}
	}

	private JarFile[] jarFiles;
	private ClassLoader cl;

	public FindClasses(File... jars) throws IOException {

		jarFiles = new JarFile[jars.length];
		URL[] urls = new URL[jars.length];
		try {
			for (int i = 0; i < jars.length; i++) {
				jarFiles[i] = new JarFile(jars[i]);
				urls[i] = jars[i].toURI().toURL();
			}
		} catch (MalformedURLException e) {
			throw new RuntimeException(
					"WTF, how can JarFile.toURL() be malformed?", e);
		}

		this.cl = new URLClassLoader(urls, this.getClass().getClassLoader());
	}

	/**
	 * returns a class loader that includes the jar used to construct this
	 * instance
	 */
	public ClassLoader getClassLoader() {
		return this.cl;
	}

	/**
	 * Find useful concrete (ie: not anonymous, not abstract, not an interface)
	 * classes that extend clazz
	 */
	public Collection<Class> findExtends(Class<?> clazz)
			throws ClassNotFoundException {

		HashSet<Class> results = new HashSet<Class>();

		for (JarFile jarFile : jarFiles) {
			for (Enumeration<JarEntry> e = jarFile.entries(); e
					.hasMoreElements();) {

				String n = e.nextElement().getName();
				if (n.endsWith(".class")) {
					String cn = n.replace("/", ".")
							.substring(0, n.length() - 6);
					Class<?> target;
					try {
						target = cl.loadClass(cn);
					} catch (NoClassDefFoundError e1) {
						throw new ClassNotFoundException("Can't load: " + cn,
								e1);
					}

					if (clazz.isAssignableFrom(target)
							&& !target.isAnonymousClass()) {

						int mods = target.getModifiers();
						if (!(Modifier.isAbstract(mods) || Modifier
								.isInterface(mods))) {
							results.add(target);
						}
					}
				}
			}
		}
		return results;
	}

	/**
	 * Given a collection of classes, returns a Map containing the subset of
	 * those classes that impliment the method specified, where the value in the
	 * map is the return type of the method
	 */
	public Map<Class, Class> findMethodReturns(Collection<Class> clazzes,
			String methodName, Class... parameterTypes)
			throws NoSuchMethodException {

		HashMap<Class, Class> results = new HashMap<Class, Class>();
		for (Class clazz : clazzes) {
			try {
				Method m = clazz.getMethod(methodName, parameterTypes);
				results.put(clazz, m.getReturnType());
			} catch (NoSuchMethodException e) {
				/* :NOOP: we expect this and skip clazz */
			}
		}
		return results;
	}
}
